package com.fary.web.servlet.mvc.method.annotation;

import com.fary.context.EmbeddedValueResolverAware;
import com.fary.core.annotation.AnnotatedElementUtils;
import com.fary.stereotype.Controller;
import com.fary.util.Assert;
import com.fary.util.CollectionUtils;
import com.fary.util.StringValueResolver;
import com.fary.web.accept.ContentNegotiationManager;
import com.fary.web.bind.annotation.CrossOrigin;
import com.fary.web.bind.annotation.RequestMapping;
import com.fary.web.bind.annotation.RequestMethod;
import com.fary.web.cors.CorsConfiguration;
import com.fary.web.method.HandlerMethod;
import com.fary.web.servlet.handler.MatchableHandlerMapping;
import com.fary.web.servlet.handler.RequestMatchResult;
import com.fary.web.servlet.mvc.condition.RequestCondition;
import com.fary.web.servlet.mvc.method.RequestMappingInfo;
import com.fary.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import jdk.internal.jline.internal.Nullable;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Predicate;

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {

    /**
     * 是否包含.*来映射请求
     * 假设RequestMapping注解中指定的路径是/test
     * 如果设置成True，那么对于/test.do,/test.a等任何包含.的请求都会映射到/test上去；
     * 如果设置成False，那么对于这种请求不会进行映射。
     */
    private boolean useSuffixPatternMatch = true;

    /**
     * 配置后置模式匹配是否仅在配置内容协商中明确指定的路径扩展名称时生效
     * 举个例子：假设WebMvcConfigurer中覆盖了configureContentNegotiation方法进行以下处理：
     *
     * @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
     * configurer.mediaType("pdf", MediaType.APPLICATION_PDF);
     * }
     * 当setUseRegisteredSuffixPatternMatch配置成TRUE时，即使setUseSuffixPatternMatch设置成True，
     * 在访问/test.do时也不会命中被RequestMapping注解值为/test的Controller；
     * 只有在访问/test.pdf时才能正常访问，其它任何的/test.txt或者/test.doc等均会报404；
     * 默认情况下该值是False
     */
    private boolean useRegisteredSuffixPatternMatch = false;

    /**
     * 设置路径后是否包含/
     * 假设RequestMapping注解中指定的路径是/test
     * 设置成True时，会同时处理/test/和/test的请求
     * 设置成False时，只会处理/test的请求
     * 默认是True
     */
    private boolean useTrailingSlashMatch = true;

    /**
     * 路径前缀
     */
    private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();

    /**
     * 内容协商管理器
     */
    private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

    private StringValueResolver embeddedValueResolver;

    private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();

    public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
        this.useSuffixPatternMatch = useSuffixPatternMatch;
    }

    public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
        this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
        this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
    }

    public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
        this.useTrailingSlashMatch = useTrailingSlashMatch;
    }

    public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
        this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
    }

    public Map<String, Predicate<Class<?>>> getPathPrefixes() {
        return this.pathPrefixes;
    }

    public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
        Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
        this.contentNegotiationManager = contentNegotiationManager;
    }

    public ContentNegotiationManager getContentNegotiationManager() {
        return this.contentNegotiationManager;
    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        this.embeddedValueResolver = resolver;
    }

    // RequestMappingHandlerMapping 通过基类潜在实现了接口 InitializingBean,
    // 该方法是接口 InitializingBean 定义的初始化化方法，会在当前bean创建过程中
    // 初始化过程中被调用.
    // 该方法的主要作用是 ：
    // 1. 创建配置对象config,并设置运行时所需要的各种参数到该配置对象
    // 2. 检测所有的控制器方法 (由基类的初始化方法提供)

    /**
     * 1.处理器映射器初始化时，调用此方法
     * 2.设置运行时所需要的各种参数到该配置对象
     * 3.扫描SpringMVC容器中所有bean,检测并注册Handler
     */
    @Override
    public void afterPropertiesSet() {
        this.config = new RequestMappingInfo.BuilderConfiguration();
        this.config.setUrlPathHelper(getUrlPathHelper());
        this.config.setPathMatcher(getPathMatcher());
        this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
        this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
        this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
        this.config.setContentNegotiationManager(getContentNegotiationManager());

        super.afterPropertiesSet();
    }


    /**
     * Whether to use suffix pattern matching.
     */
    public boolean useSuffixPatternMatch() {
        return this.useSuffixPatternMatch;
    }

    /**
     * Whether to use registered suffixes for pattern matching.
     */
    public boolean useRegisteredSuffixPatternMatch() {
        return this.useRegisteredSuffixPatternMatch;
    }

    /**
     * Whether to match to URLs irrespective of the presence of a trailing slash.
     */
    public boolean useTrailingSlashMatch() {
        return this.useTrailingSlashMatch;
    }

    /**
     * Return the file extensions to use for suffix pattern matching.
     */
    public List<String> getFileExtensions() {
        return this.config.getFileExtensions();
    }


    /**
     * {@inheritDoc}
     * <p>Expects a handler to have either a type-level @{@link Controller}
     * annotation or a type-level @{@link RequestMapping} annotation.
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        // 1）从处理方法中读取 RequestMapping 信息并创建 RequestMappingInfo
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            // 2）从处理器类中读取 RequestMapping 信息并创建 RequestMappingInfo
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                // 如果存在，则合并
                info = typeInfo.combine(info);
            }
            // 3）如果给处理类上配置了前缀路径 WebMvcConfigurer.configurePathMatch
            String prefix = getPathPrefix(handlerType);
            if (prefix != null) {
                // 则完成路径拼接
                info = RequestMappingInfo.paths(prefix).build().combine(info);
            }
        }
        return info;
    }

    /**
     * 获取前置路径
     */
    String getPathPrefix(Class<?> handlerType) {
        for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
            if (entry.getValue().test(handlerType)) {
                String prefix = entry.getKey();
                if (this.embeddedValueResolver != null) {
                    prefix = this.embeddedValueResolver.resolveStringValue(prefix);
                }
                return prefix;
            }
        }
        return null;
    }

    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        // 读取注解元素上的 RequestMapping 注解信息
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        /**
         * 1）如果是 class，则通过 getCustomTypeCondition 读取 RequestCondition
         * 2）如果是 method，则通过 getCustomMethodCondition 读取 RequestCondition
         *  特性未实现，都返回 null
         */
        RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    }

    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return null;
    }

    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return null;
    }

    protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, RequestCondition<?> customCondition) {

        RequestMappingInfo.Builder builder = RequestMappingInfo
                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name());
        if (customCondition != null) {
            builder.customCondition(customCondition);
        }
        return builder.options(this.config).build();
    }

    /**
     * Resolve placeholder values in the given array of patterns.
     *
     * @return a new array with updated patterns
     */
    protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
        if (this.embeddedValueResolver == null) {
            return patterns;
        } else {
            String[] resolvedPatterns = new String[patterns.length];
            for (int i = 0; i < patterns.length; i++) {
                resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
            }
            return resolvedPatterns;
        }
    }

    @Override
    public RequestMatchResult match(HttpServletRequest request, String pattern) {
        RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
        RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
        if (matchingInfo == null) {
            return null;
        }
        Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
    }

    @Override
    protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        Class<?> beanType = handlerMethod.getBeanType();
        CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
        CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

        if (typeAnnotation == null && methodAnnotation == null) {
            return null;
        }

        CorsConfiguration config = new CorsConfiguration();
        updateCorsConfig(config, typeAnnotation);
        updateCorsConfig(config, methodAnnotation);

        // 若@CrossOrigin未标准HTTP方法，则以@RequestMapping标准HTTP方法为准.
        if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
            for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
                config.addAllowedMethod(allowedMethod.name());
            }
        }
        // 为未初始化的配置设置默认值.
        return config.applyPermitDefaultValues();
    }

    private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {
        // 注解为空，跳过处理.
        if (annotation == null) {
            return;
        }
        // 解析@CrossOrigin.origins属性到配置.
        for (String origin : annotation.origins()) {
            config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
        }
        // 解析@CrossOrigin.methods属性到配置.
        for (RequestMethod method : annotation.methods()) {
            config.addAllowedMethod(method.name());
        }
        // 解析@CrossOrigin.allowedHeaders属性到配置.
        for (String header : annotation.allowedHeaders()) {
            config.addAllowedHeader(resolveCorsAnnotationValue(header));
        }
        // 解析@CrossOrigin.exposedHeaders属性到配置.
        for (String header : annotation.exposedHeaders()) {
            config.addExposedHeader(resolveCorsAnnotationValue(header));
        }
        // 解析@CrossOrigin.allowCredentials属性到配置.
        String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());
        if ("true".equalsIgnoreCase(allowCredentials)) {
            config.setAllowCredentials(true);
        } else if ("false".equalsIgnoreCase(allowCredentials)) {
            config.setAllowCredentials(false);
        } else if (!allowCredentials.isEmpty()) {
            throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " +
                    "or an empty string (\"\"): current value is [" + allowCredentials + "]");
        }
        // 解析@CrossOrigin.maxAge属性到配置.
        if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
            config.setMaxAge(annotation.maxAge());
        }
    }

    private String resolveCorsAnnotationValue(String value) {
        if (this.embeddedValueResolver != null) {
            String resolved = this.embeddedValueResolver.resolveStringValue(value);
            return (resolved != null ? resolved : "");
        } else {
            return value;
        }
    }

}